# -------------------------------------------------------------------------------------------------
#  Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
#  https://nautechsystems.io
#
#  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
#  You may not use this file except in compliance with the License.
#  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
# -------------------------------------------------------------------------------------------------

from decimal import Decimal
from collections import deque

from nautilus_trader.common.enums import LogColor
from nautilus_trader.config import PositiveFloat
from nautilus_trader.config import PositiveInt
from nautilus_trader.config import StrategyConfig
from nautilus_trader.core.correctness import PyCondition
from nautilus_trader.core.data import Data
from nautilus_trader.core.message import Event
from nautilus_trader.indicators.atr import AverageTrueRange
from nautilus_trader.indicators.average.ema import ExponentialMovingAverage
from nautilus_trader.model.book import OrderBook
from nautilus_trader.model.data import Bar
from nautilus_trader.model.data import BarType
from nautilus_trader.model.data import QuoteTick
from nautilus_trader.model.data import TradeTick
from nautilus_trader.model.enums import OrderSide
from nautilus_trader.model.enums import TimeInForce
from nautilus_trader.model.enums import TrailingOffsetType
from nautilus_trader.model.enums import TriggerType
from nautilus_trader.model.events import OrderFilled
from nautilus_trader.model.identifiers import InstrumentId
from nautilus_trader.model.instruments import Instrument
from nautilus_trader.model.objects import Price
from nautilus_trader.model.orders import MarketIfTouchedOrder, MarketOrder
from nautilus_trader.model.orders import TrailingStopMarketOrder
from nautilus_trader.trading.strategy import Strategy
from nautilus_trader.model.currencies import USDT
from nautilus_trader.model.enums import PositionSide
from nautilus_trader.model.enums import PriceType
from nautilus_trader.model.events import PositionEvent, PositionChanged, PositionClosed, PositionOpened

from cryptoAlpha.indicators.mmax import MMax
from cryptoAlpha.indicators.mmin import MMin
from cryptoAlpha.indicators.move import Move
from cryptoAlpha.indicators.hisBar import HisBar


# *** THIS IS A TEST STRATEGY WITH NO ALPHA ADVANTAGE WHATSOEVER. ***
# *** IT IS NOT INTENDED TO BE USED TO TRADE LIVE WITH REAL MONEY. ***


class PivotStrategyConfig(StrategyConfig, frozen=True):
    """
    Configuration for ``PivotStrategy`` instances.

    Parameters
    ----------
    instrument_id : InstrumentId
        The instrument ID for the strategy.
    barType : BarType
        The bar type for the strategy.
    """

    instrument_id: InstrumentId
    barType: BarType

class PivotStrategy(Strategy):
    """
    A simple moving average cross example strategy with a `MARKET_IF_TOUCHED` entry and
    `TRAILING_STOP_MARKET` stop.

    When the fast EMA crosses the slow EMA then submits a `MARKET_IF_TOUCHED` order
    one tick above the current bar for BUY, or one tick below the current bar
    for SELL.

    If the entry order is filled then a `TRAILING_STOP_MARKET` at a specified
    ATR distance is submitted and managed.

    Cancels all orders and closes all positions on stop.

    Parameters
    ----------
    config : PivotStrategyConfig
        The configuration for the instance.

    Raises
    ------
    ValueError
        If `config.fast_ema_period` is not less than `config.slow_ema_period`.

    """

    def __init__(self, config: PivotStrategyConfig) -> None:
        super().__init__(config)
        
        self.instrument_id = config.instrument_id
        self.barType = config.barType
        self.posNum = 10
        self.left = 20
        self.right = 20
        
        self.emulation_trigger = TriggerType.NO_TRIGGER
        
        self.atr = AverageTrueRange(24)
        self.fast_ema = ExponentialMovingAverage(30)
        self.slow_ema = ExponentialMovingAverage(60)
        self.mmaxLots = MMax(1000)
        self.mminLots = MMin(1000)
        self.mmaxAtr = MMax(720)
        self.mminAtr = MMin(720)
        self.hh = MMax(2000)
        self.ll = MMin(2000)
        self.movePrice = Move(1000)
        self.pivotBar = HisBar(self.left + self.right + 1)
        
        self.instrument: Instrument | None = None  # Initialized in on_start
        self.venue = self.instrument_id.venue
        
        # var
        self.highP = None
        self.lowP = None
        self.breakUp = None
        self.break_down = None
        self.openLong = False
        self.openShort = False
        self.litterDnP = False
        self.litterUpP = False
        self.preHighP = None
        self.preLowP = None
        self.highVol = None
        self.lowVol = None
        
        # watcher
        self.params = [
            "lots",
            "p"
            "leftHigh",
            "rightHigh",
            "leftLow",
            "rightLow",
            "highP",
            "lowP",
            "highIf",
            "lowIf",
        ]

    def on_start(self) -> None:
        """
        Actions to be performed on strategy start.
        """
        self.instrument = self.cache.instrument(self.instrument_id)
        if self.instrument is None:
            self.log.error(f"Could not find instrument for {self.instrument_id}")
            self.stop()
            return

        # Register the indicators for updating
        self.register_indicator_for_bars(self.barType, self.atr)
        self.register_indicator_for_bars(self.barType, self.fast_ema)
        self.register_indicator_for_bars(self.barType, self.slow_ema)
        self.register_indicator_for_bars(self.barType, self.mmaxLots)
        self.register_indicator_for_bars(self.barType, self.mminLots)
        self.register_indicator_for_bars(self.barType, self.mmaxAtr)
        self.register_indicator_for_bars(self.barType, self.mminAtr)
        self.register_indicator_for_bars(self.barType, self.movePrice)
        self.register_indicator_for_bars(self.barType, self.hh)
        self.register_indicator_for_bars(self.barType, self.ll)
        self.register_indicator_for_bars(self.barType, self.pivotBar)
        
        # Get historical data
        self.request_bars(self.barType, callback=lambda uuid:print(uuid))

        # Subscribe to live data
        self.subscribe_bars(self.barType)
        self.subscribe_quote_ticks(self.instrument_id)
        
        self.account = self.portfolio.account(self.venue)
        self.mintick = self.instrument.price_increment.as_double()
        self.lim = 14 * self.mintick
        self.pos = self.portfolio.net_position(self.instrument.id)
        return

    def on_instrument(self, instrument: Instrument) -> None:
        """
        Actions to be performed when the strategy is running and receives an instrument.

        Parameters
        ----------
        instrument : Instrument
            The instrument received.

        """
        return
    
    def on_historical_data(self, data: Data) -> None:
        return

    def on_order_book(self, order_book: OrderBook) -> None:
        """
        Actions to be performed when the strategy is running and receives an order book.

        Parameters
        ----------
        order_book : OrderBook
            The order book received.

        """
        # self.log.info(f"Received {order_book}")  # For debugging (must add a subscription)

    def on_quote_tick(self, tick: QuoteTick) -> None:
        """
        Actions to be performed when the strategy is running and receives a quote tick.

        Parameters
        ----------
        tick : QuoteTick
            The tick received.

        """

    def on_trade_tick(self, tick: TradeTick) -> None:
        """
        Actions to be performed when the strategy is running and receives a trade tick.

        Parameters
        ----------
        tick : TradeTick
            The tick received.

        """

    def on_bar(self, bar: Bar) -> None:
        """
        Actions to be performed when the strategy is running and receives a bar.

        Parameters
        ----------
        bar : Bar
            The bar received.

        """
        self.log.info(f"Received {bar!r}")
        # Check if indicators ready
        if not self.indicators_initialized():
            self.log.info(
                f"Waiting for indicators to warm up [{self.cache.bar_count(self.barType)}]",
                color=LogColor.BLUE,
            )
            return  # Wait for indicators to warm up...
        
        # 滚动计算一个波动敞口
        fund = self.account.balance_total(USDT).as_double() / self.posNum
        range = self.mmaxLots.value - self.mminLots.value
        if range > 10 * self.mintick:
            self.lots = fund / range
        atr = self.mmaxAtr.value - self.mminAtr.value
        
        # 最小开仓振幅
        self.tradeIf = (self.hh.value - self.ll.value) > self.lim
        
        # 计算pivot
        self.leftHigh = max(list(self.pivotBar.high)[:self.left])
        self.rightHigh = max(list(self.pivotBar.high)[-self.right:])
        self.leftLow = min(list(self.pivotBar.low)[:self.left])
        self.rightLow = min(list(self.pivotBar.low)[-self.right:])
        
        pH = self.pivotBar.high[self.left]
        pL = self.pivotBar.low[self.left]
        if pH > self.leftHigh and pH > self.rightHigh:
            self.preHighP = self.highP
            self.highP = pH
            self.preHighVol = self.highVol
            self.highVol = self.highP - self.ll.value
            
        if pL < self.leftLow and pL < self.rightLow:
            self.preLowP = self.lowP
            self.lowP =pL
            self.preLowVol = self.lowVol
            self.lowVol = self.hh.value - self.lowP
        
        # 顶点抬升，且宽度增加
        self.upP = self.highP and self.preHighP and self.highVol and self.preHighVol\
            and self.highP > self.preHighP and self.highVol > self.preHighVol
        self.dnP = self.lowP and self.preLowP and self.lowVol and self.preLowVol\
            and self.lowP < self.preLowP and self.lowVol > self.preLowVol
        
        if self.upP:
            self.breakUp = self.highP
        if self.dnP:
            self.break_down = self.lowP
        
        # 且回调幅度小
        self.litterDnP = self.lowVol and self.upP and self.lowVol < atr*0.5
        self.litterUpP = self.highVol and self.dnP and self.highVol < atr*0.5
        
        # 开仓逻辑
        high = bar.high.as_double()
        low = bar.low.as_double()
        close = bar.close.as_double()
        
        self.openLong = self.breakUp and high >=self.breakUp and self.litterDnP and close < self.breakUp
        self.openShort = self.break_down and low <= self.break_down and self.litterUpP and close > self.break_down
        
        if self.openLong and self.pos <= 0:
            self.set_position(self.lots)
        if self.openShort and self.pos >= 0:
            self.set_position(-self.lots)
    
        return

    def set_position(self, target: float) -> None:
        pos = self.portfolio.net_position(self.instrument.id)
        diff = target - float(pos)
        if abs(diff - 0) < self.instrument.size_increment * 5:
            return
        if diff > 0:
            order: MarketOrder = self.order_factory.market(
                instrument_id=self.instrument_id,
                order_side=OrderSide.BUY,
                quantity=self.instrument.make_qty(abs(diff)),
                time_in_force=TimeInForce.IOC,
            )
        else:
            order: MarketOrder = self.order_factory.market(
                instrument_id=self.instrument_id,
                order_side=OrderSide.SELL,
                quantity=self.instrument.make_qty(abs(diff)),
                time_in_force=TimeInForce.IOC,
            )
        
        self.submit_order(order)
        
        # lastBar = self.cache.last_bar(self.barType)
        # if diff > 0:
            
        
        #             and self.pos <= 0:
        #     if self.pos < 0:
        #         self.close_all_positions(self.instrument_id, position_side=PositionSide.SHORT)
        #     self.entry_buy(bar)
        # if self.openShort and self.pos >= 0:
        #     if self.pos > 0:
        #         self.close_all_positions(self.instrument_id, position_side=PositionSide.LONG)
        #     self.entry_sell(bar)
        

    def entry_buy(self, last_bar: Bar) -> None:
        """
        Users simple buy entry method (example).

        Parameters
        ----------
        last_bar : Bar
            The last bar received.

        """
        if not self.instrument:
            self.log.error("No instrument loaded")
            return

        if not self.mintick:
            self.log.error("No tick size loaded")
            return

        order: MarketOrder = self.order_factory.market(
            instrument_id=self.instrument_id,
            order_side=OrderSide.BUY,
            quantity=self.instrument.make_qty(self.lots),
            time_in_force=TimeInForce.IOC,
        )

        # order: MarketIfTouchedOrder = self.order_factory.market_if_touched(
        #     instrument_id=self.instrument_id,
        #     order_side=OrderSide.BUY,
        #     quantity=self.instrument.make_qty(self.lots),
        #     time_in_force=TimeInForce.IOC,
        #     trigger_price=self.instrument.make_price(last_bar.high + (self.mintick * 2)),
        #     emulation_trigger=self.emulation_trigger,
        # )
        # TODO: Uncomment below order for development
        # order: LimitIfTouchedOrder = self.order_factory.limit_if_touched(
        #     instrument_id=self.instrument_id,
        #     order_side=OrderSide.BUY,
        #     quantity=self.instrument.make_qty(self.trade_size),
        #     time_in_force=TimeInForce.IOC,
        #     price=self.instrument.make_price(last_bar.low - (self.tick_size * 2)),
        #     trigger_price=self.instrument.make_price(last_bar.high + (self.tick_size * 2)),
        # )

        self.entry = order
        self.submit_order(order)

    def entry_sell(self, last_bar: Bar) -> None:
        """
        Users simple sell entry method (example).

        Parameters
        ----------
        last_bar : Bar
            The last bar received.

        """
        if not self.instrument:
            self.log.error("No instrument loaded")
            return

        if not self.mintick:
            self.log.error("No tick size loaded")
            return

        order: MarketOrder = self.order_factory.market(
            instrument_id=self.instrument_id,
            order_side=OrderSide.SELL,
            quantity=self.instrument.make_qty(self.lots),
            time_in_force=TimeInForce.IOC,
        )
        # order: MarketIfTouchedOrder = self.order_factory.market_if_touched(
        #     instrument_id=self.instrument_id,
        #     order_side=OrderSide.SELL,
        #     quantity=self.instrument.make_qty(self.lots),
        #     time_in_force=TimeInForce.IOC,
        #     trigger_price=self.instrument.make_price(last_bar.high + (self.mintick * 2)),
        #     emulation_trigger=self.emulation_trigger,
        # )
        # TODO: Uncomment below order for development
        # order: LimitIfTouchedOrder = self.order_factory.limit_if_touched(
        #     instrument_id=self.instrument_id,
        #     order_side=OrderSide.SELL,
        #     quantity=self.instrument.make_qty(self.trade_size),
        #     time_in_force=TimeInForce.IOC,
        #     price=self.instrument.make_price(last_bar.low - (self.tick_size * 2)),
        #     trigger_price=self.instrument.make_price(last_bar.low - (self.tick_size * 2)),
        # )
        self.entry = order
        self.submit_order(order)
        return

    def trailing_stop_buy(self) -> None:
        """
        Users simple trailing stop BUY for (``SHORT`` positions).
        """
        return

    def trailing_stop_sell(self) -> None:
        """
        Users simple trailing stop SELL for (LONG positions).
        """
        return

    def on_data(self, data: Data) -> None:
        """
        Actions to be performed when the strategy is running and receives data.

        Parameters
        ----------
        data : Data
            The data received.

        """
        return
    
    def on_position_event(self, event: PositionEvent):
        # 仓位变更时，重置信号
        if isinstance(event, PositionChanged|PositionOpened):
            self.pos = self.portfolio.net_position(self.instrument.id)
            if self.pos > 0:
                self.breakUp = None
            elif self.pos < 0:
                self.break_down = None
                

    def on_event(self, event: Event) -> None:
        """
        Actions to be performed when the strategy is running and receives an event.

        Parameters
        ----------
        event : Event
            The event received.

        """
        return

    def on_stop(self) -> None:
        """
        Actions to be performed when the strategy is stopped.
        """
        return

    def on_reset(self) -> None:
        """
        Actions to be performed when the strategy is reset.
        """
        # Reset indicators here
        return

    def on_save(self) -> dict[str, bytes]:
        """
        Actions to be performed when the strategy is saved.

        Create and return a state dictionary of values to be saved.

        Returns
        -------
        dict[str, bytes]
            The strategy state dictionary.

        """
        return {}

    def on_load(self, state: dict[str, bytes]) -> None:
        """
        Actions to be performed when the strategy is loaded.

        Saved state values will be contained in the give state dictionary.

        Parameters
        ----------
        state : dict[str, bytes]
            The strategy state dictionary.

        """
        return

    def on_dispose(self) -> None:
        """
        Actions to be performed when the strategy is disposed.

        Cleanup any resources used by the strategy here.

        """
        return
